# Copyright (C) 2010 Zajcev Evgeny <zevlg@yandex.ru>

# Very simple class for input/output with minimal, but sufficient,
# support of xml-pull-parsing.

import os, sys

API_ORIGIN = "http://www.openstreetmap.org/api"

# Create attibute patterns for various circumstances
attrnames = []
cdef tagattr* init_attrs(pattern):
    cdef int i = 0
    cdef tagattr* ra = <tagattr*>malloc(sizeof(tagattr)*(len(pattern)+1))
    for n,t in pattern:
        taname = ' '+n+'="'
        attrnames.append(taname)
        ra[i].name, ra[i].type = taname, t
        ra[i].nlen = len(taname)
        i += 1

    ra[i].type = AEND
    return ra

# for <tag id=
cdef tagattr* id_attr = init_attrs([('id', AINT)])
# for <node lat= lon=
cdef tagattr* latlon_attr = init_attrs([('lat', ADBL), ('lon', ADBL)])
# for <nd ref=, <member ref=
cdef tagattr* ref_attr = init_attrs([('ref', AINT)])
# for <member type=
cdef tagattr* type_attr = init_attrs([('type', ASTR)])
# for <tag version=
cdef tagattr* version_attr = init_attrs([('version', AINT)])

# Spicy to search for closed tags
cdef char clnode[128]
clnode[0] = c'<'; clnode[1] = c'/'

DEF RBSIZE = 204800
DEF WBSIZE = RBSIZE

cdef class BIO:
    cdef:
        object file, header, mode, filesize
        unsigned long long progress, limit
        char* buff
        size_t bo, bsz                  # offset in the buff and usable size
        int fd

    def __init__(self, file, mode='r'):
        self.mode = mode
        if file == "-" and mode == "w":
            self.file = sys.stdout
        else:
            self.file = open(file, mode)
        self.fd = self.file.fileno()
        self.limit = self.filesize = os.fstat(self.fd).st_size
        self.bo = self.progress = 0

        self.bsz = RBSIZE if mode == 'r' else WBSIZE
        self.buff = <char*>malloc(self.bsz)
        if self.mode == 'r':
            rdahead(self.fd)
            # NOTE: seek does initial read and skips header
            self.seek(0)

    def __dealloc__(self):
        free(self.buff)

    def close(self):
        if self.file != sys.stdout:
            self.file.close()

    cdef inline int chars_left(self):
        return self.bsz - self.bo

    ### Reading methods, for 'r' mode only
    cdef void map_tags(self, cbfunc fun, sobj):
        """For each tag obtained from ``read_tag()`` apply FUN."""
        cdef tinfo tag = self.read_tag()
        while tag.type is not PARTIAL and \
                  self.limit >= self.progress + (tag.e-self.buff):
            self.bo = tag.e - self.buff
            fun(sobj, &tag)
            tag = self.read_tag()

    cdef tinfo read_tag(self):
        """Read next tag from buffer.
        It might read portion of data from file."""
        cdef tinfo tag = self.parse_tag(&self.buff[self.bo], \
                                        &self.buff[self.bsz])
        if tag.type is not PARTIAL: return tag

        # Read a portion of data from file
        cdef int cl = self.chars_left()
        memmove(self.buff, &self.buff[self.bo], cl)
        self.bsz = cl + read(self.fd, &self.buff[cl], self.bo)
        self.progress += self.bo
        self.bo = 0

        return self.parse_tag(self.buff, &self.buff[self.bsz])

    cdef tinfo parse_tag(self, char* start, char* end):
        """Extract tag from buffer denoted by START and END.
        Return tag, in case tag is not extracted then ``tag.type`` is
        PARTIAL."""
        cdef tinfo tag
        cdef char *ss, *tagname

        tag.type = PARTIAL
        tag.s = tag.e = tag.h = NULL

        tag.s = ss = strnstr(start, '<', end-start)
        if not ss: return tag
        tagname = ss + 1
        ss = strnstr(tagname, " ", end-tagname)
        if not ss: ss = strnstr(tagname, "\t", end-tagname)
        if not ss: return tag

        cdef int tnl = ss - tagname
        # search for closing tag before comparing tagname
        ss = strnstr(ss, ">", end-ss)
        if not ss: return tag
        tag.h = ss + 1
        if ss[-1] == '/':
            tag.e = tag.h
        else:
            # check for </tagname> closing
            memcpy(clnode+2, tagname, tnl)
            clnode[tnl+2] = c'>'; clnode[tnl+3] = c'\0'
            ss = strnstr(ss, clnode, end-ss)
            if not ss: return tag
            tag.e = ss + strlen(clnode)

        if memcmp(tagname, "node", tnl) == 0:
            tag.type = NODE
        elif memcmp(tagname, "way", tnl) == 0:
            tag.type = WAY
        elif memcmp(tagname, "nd", tnl) == 0:
            tag.type = ND
        elif memcmp(tagname, "relation", tnl) == 0:
            tag.type = RELATION
        elif memcmp(tagname, "member", tnl) == 0:
            tag.type = MEMBER
        else:
            tag.type = UNKNOWN

        return tag

    cdef int parse_attrs(self, tinfo* tag, tagattr* attr):
        """Parse attributes string for given TAG."""
        cdef char *ssn, *sn = tag.s
        while attr.type is not AEND:
            ssn = strnstr(sn, attr.name, tag.h-tag.s)
            if not ssn:
                if sn == tag.s:
                    return -1
                # Research from the beginning
                sn = tag.s
                continue

            sn = ssn + attr.nlen
            if attr.type is AINT:
                attr.value.integer = strtol(sn, NULL, 10)
            elif attr.type is ADBL:
                attr.value.floating = strtod(sn, NULL)
            elif attr.type is ASTR:
                attr.value.string = sn-1

            attr += 1
        return 0

    cdef inline int tagattr_intval(self, tinfo* tag, tagattr* ta):
        self.parse_attrs(tag, ta)
        return ta[0].value.integer

    cdef inline int tagattr_id(self, tinfo* tag):
        return self.tagattr_intval(tag, id_attr)

    cdef inline int tagattr_ref(self, tinfo* tag):
        return self.tagattr_intval(tag, ref_attr)

    cdef inline latlon tagattr_latlon(self, tinfo* tag):
        cdef latlon ll
        cdef char* ss = tag.s
        # lat/lon tends to lie at the end, try to search from the
        # middle, then fallback to the beginning
        if tag.h - tag.s > 40:
            tag.s = tag.h - 40
        if self.parse_attrs(tag, latlon_attr) == -1:
            tag.s = ss
            self.parse_attrs(tag, latlon_attr)
        tag.s = ss
        ll.lat = latlon_attr[0].value.floating
        ll.lon = latlon_attr[1].value.floating
        return ll

    cdef inline char* tagattr_type(self, tinfo* tag):
        self.parse_attrs(tag, type_attr)
        return type_attr[0].value.string

    cdef inline int tagattr_version(self, tinfo* tag):
        self.parse_attrs(tag, version_attr)
        return version_attr[0].value.integer

    def skip_header(self):
        # Skip initial <?xml and <osm
        cdef char* ss
        ss = strnstr(self.buff, "<osm ", self.bsz)
        if not ss: return
        ss = strnstr(ss+3, ">", self.bsz-(ss-self.buff))
        if not ss: return
        self.bo = ss - self.buff + 1

        self.header = self.buff[:self.bo]

    def seek(self, position):
        """Seek a POSITION inside the file."""
        os.lseek(self.fd, position, os.SEEK_SET)
        self.progress = position
        if self.mode == 'r':
            self.bo = 0
            self.bsz = read(self.fd, self.buff, RBSIZE)
            if position is 0: self.skip_header()
        else:
            write(self.fd, self.buff, self.bo)
            os.ftruncate(self.fd, position)
            self.bo = 0

    ### Saving methods, for 'w' mode only
    def save_header(self, Polygon poly, BIO source):
        import re
        xml, vl = source.header.split("\n")
        vm = re.search('osm version="([^"]+)"', vl)
        ver = vm.group(1) if vm else "0.6"
        self.file.write('%s\n'%xml)
        self.file.write('<osm version="%s" generator="Osmtrunc v%s">\n'% \
                        (ver, __version__))
        self.file.write('  <bound box="%f,%f,%f,%f"'%poly.bounds())
        self.file.write(' origin="%s/%s"/>\n'%(API_ORIGIN, ver))
        self.file.flush()

    def save_footer(self):
        # Flush pending data
        write(self.fd, self.buff, self.bo)
        self.file.write("</osm>\n")
        self.file.flush()

    cdef void save_tag(self, tinfo* tag, BIO source):
        """Save TAG obtained from SOURCE to the file.
        Pretty printing is applied."""
        cdef int td = tag.e - tag.s, soff = 2
        cdef char* spacing = "  "

        # special offset for <nd, <member and unrecognized tags
        if tag.type is UNKNOWN or tag.type is ND or tag.type is MEMBER:
            spacing = "    "
            soff = 4

        if td > self.chars_left()-soff-1:
            if self.bo != write(self.fd, self.buff, self.bo):
                # Not actually wrong, but too lazy to workaround
                raise "Something wrong while writing to file"
            self.bo = 0

        cdef char* sbo = &self.buff[self.bo]
        memcpy(sbo, spacing, soff)
        memcpy(sbo+soff, tag.s, td)
        memcpy(sbo+soff+td, "\n", 1)
        self.bo += td + soff + 1
